using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;

namespace CHIP8Emulator {
    /* CHIP8 machine is defined by CHIP8Device class */
    public class CHIP8Device {
        private readonly Logger _logger; // Log debug messages
        private BackgroundWorker _worker; // Worker to run device async
        private readonly OpcodeProcessor[] _opcodeProcessors = new OpcodeProcessor[Enum.GetValues(typeof(OpcodeType)).Length]; // All opcodes are split into groups. Each group is handled by the corresponding OpcodeProcessor
        public event EventHandler OnCompleted;

        /* Machine specs */
        public const int Y_RESOLUTION = 32;
        public const int X_RESOLUTION = 64;
        public const ushort PC_INIT_VALUE = 0x200; // Initial value of the program counter
        public const int VIDEO_MEMORY_SIZE = X_RESOLUTION * Y_RESOLUTION;
        private const int REGISTER_FILE_SIZE = 16;
        public const int STACK_SIZE = 12;
        public const int RAM_SIZE = 0x1000;

        public const ushort OPCODE_SIZE = 2; // 2 bytes

        /* Keyboard */
        public const int KEYS_NUM = 0x10;
        private readonly bool[] _keys = new bool[KEYS_NUM];
        
        /* Video */
        private const int SPRITE_SIZE_X = 8; // Width of sprite in pixels
        private const ushort FONT_ADDRESS = 0x080; // Memory location of font
        private const int SYMBOL_HEIGHT = 5; // Font symbol is 5 pixels high
        private const uint WHITE_COLOR_CODE = 0x0000AA00; // Well, it's a green actually
        private const int BLACK_COLOR_CODE = 0x00000000;


        /* Device parts */
        public byte[] M { get; private set; } // RAM
        public uint[] D { get; set; } // Video memory
        public ushort I { get; set; } // Address register
        public byte[] V { get; private set; } // Register file
        public Stack<ushort> Stack { get; private set; } // Stack
        public byte DelayTimer { get; set; } // Delay timer. Ticks down from 256 to 0 each cycle.
        public byte SoundTimer { get; set; } // Sound timer. Ticks down from 256 to 0 each cycle. If > 0 then *beep*.
        public ushort PC { get; set; } // Program counter


        /* The rest fields are used for measure performance */
        private long _ops;
        private double _startTime;
        private readonly Stopwatch _stopwatch = new Stopwatch();
        private const int CYCLES_PER_OP = 16;
        private const int CHIP8_FREQ = 1790000;
        private const int MAX_OPS_PER_SEC = CHIP8_FREQ / CYCLES_PER_OP;
        private int _maxOpsPerSec = MAX_OPS_PER_SEC;
        private readonly TimeSpan _timeout = new TimeSpan(10L);


        public CHIP8Device(Logger logger) {
            _logger = logger;
            _logger.Log(LogMessageType.Info, "Device created");
            InitWorker();
            OnCompleted += (sender, e) => { };
            CreateOpcodeProcessors();
        }

        private void InitWorker() {
            _worker = new BackgroundWorker {WorkerSupportsCancellation = true,};
            _worker.DoWork += RunProgram;
            _worker.RunWorkerCompleted += ReportCompleted;
        }

        private void ReportCompleted(object sender, RunWorkerCompletedEventArgs e) {
            _logger.Log(LogMessageType.Info, "Total time: {0}. Total operations: {1}. Operations per second: {2}", string.Format("{0:D2}:{1:D2}:{2:D2}.{3:D3}", _stopwatch.Elapsed.Hours, _stopwatch.Elapsed.Minutes, _stopwatch.Elapsed.Seconds, _stopwatch.Elapsed.Milliseconds), _ops, OpsPerSec);
            OnCompleted(sender, e);
        }

        /* This method executes program loaded into Memory starting with PC address. */
        private void RunProgram(object sender, DoWorkEventArgs e) {
            try {
                _stopwatch.Reset();
                _stopwatch.Start();
                _startTime = _stopwatch.Elapsed.TotalSeconds;
                _ops = 0L;
                _logger.Log(LogMessageType.Info, "Program started");
                while (!_worker.CancellationPending) { // Until we are cancelled
                    ushort opcode = (ushort) (M[PC + 0] << 8 | M[PC + 1]); // Get next command
                    Process(opcode); // Process command

                    CorrectClockFrequency(); // Wait a bit if we are too fast
                    SleepIfPaused();
                }
            }
            catch (Exception ex) {
                _logger.Log(ex);
                Stop();
            }
        }

        private void SleepIfPaused() {
            if (!Paused || _worker.CancellationPending) { /* Do nothing */}
            else
                while (Paused && !_worker.CancellationPending)
                    Thread.Sleep(_timeout);
        }

        private void CorrectClockFrequency() {
            _ops++;
            if (OpsPerSec < MaxOpsPerSec) { /* Do nothing */ }
            else
                while (OpsPerSec > MaxOpsPerSec) // While number of operations per second greater then its max value
                    Thread.Sleep(_timeout); // Sleep
        }

        public int MaxOpsPerSec {
            get { return _maxOpsPerSec; }
            set { _maxOpsPerSec = value; }
        }

        protected int OpsPerSec { // Operations per second
            get { return (int)(_ops / (_stopwatch.Elapsed.TotalSeconds - _startTime)); }
        }

        private void Process(ushort opcode) {
            _opcodeProcessors[opcode >> 12].Process(opcode);
        }

        private void CreateOpcodeProcessors() {
            foreach (OpcodeType opcodeType in Enum.GetValues(typeof (OpcodeType)))
                _opcodeProcessors[(int) opcodeType] = CreateProcessor(opcodeType);
        }

        private OpcodeProcessor CreateProcessor(OpcodeType opcodeType) {
            /* 
             * Opcode is 2 bytes long.
             * All the opcodes are slpit into 16 gpoups, with 4 leading bits defining group ID.
             * NNN - 12 bit number.
             * NN  -  8 bit number.
             * X, Y - 4 bit number.
             * 0-F - parts of opcode.
             * _ - ignored part of opcode.
             */
            switch (opcodeType) {
            case OpcodeType.Type_0x0:
                return new OpcodeProcessor_0NNN_00E0_00EE(this, opcodeType);
            case OpcodeType.Type_0x1:
                return new OpcodeProcessor_1NNN(this, opcodeType);
            case OpcodeType.Type_0x2:
                return new OpcodeProcessor_2NNN(this, opcodeType);
            case OpcodeType.Type_0x3:
                return new OpcodeProcessor_3XNN(this, opcodeType);
            case OpcodeType.Type_0x4:
                return new OpcodeProcessor_4XNN(this, opcodeType);
            case OpcodeType.Type_0x5:
                return new OpcodeProcessor_5XY0(this, opcodeType);
            case OpcodeType.Type_0x6:
                return new OpcodeProcessor_6XNN(this, opcodeType);
            case OpcodeType.Type_0x7:
                return new OpcodeProcessor_7XNN(this, opcodeType);
            case OpcodeType.Type_0x8:
                return new OpcodeProcessor_8XY_(this, opcodeType);
            case OpcodeType.Type_0x9:
                return new OpcodeProcessor_9XY0(this, opcodeType);
            case OpcodeType.Type_0xA:
                return new OpcodeProcessor_ANNN(this, opcodeType);
            case OpcodeType.Type_0xB:
                return new OpcodeProcessor_BNNN(this, opcodeType);
            case OpcodeType.Type_0xC:
                return new OpcodeProcessor_CXNN(this, opcodeType);
            case OpcodeType.Type_0xD:
                return new OpcodeProcessor_DXYN(this, opcodeType);
            case OpcodeType.Type_0xE:
                return new OpcodeProcessor_EX9E_EXA1(this, opcodeType);
            case OpcodeType.Type_0xF:
                return new OpcodeProcessor_FX__(this, opcodeType);
            default:
                throw new Exception(string.Format("Invalid opcode ({0}) type requested to process", opcodeType));
            }
        }

        public void Init(ushort addr) {
            _logger.Log(LogMessageType.Info, "Initialization started");
            InitMemory();
            InitStack();
            InitRegisterFile();
            InitVideoMemory();

            SetPCI(addr);

            ClearTimers();
            _logger.Log(LogMessageType.Info, "Device initialized");
        }

        public void ClearTimers() {
            DelayTimer = 0;
            SoundTimer = 0;
            _logger.Log(LogMessageType.Info, "Delay and sound timers initialized");
        }

        public void SetPCI(ushort addr) {
            PC = I = addr;
            _logger.Log(LogMessageType.Info, "Program counter and address register initialized");
        }

        public void InitVideoMemory() {
            D = new uint[VIDEO_MEMORY_SIZE]; // Black screen
            for (int y = 0; y < Y_RESOLUTION; y++) {
                for (int x = 0; x < X_RESOLUTION; x++) {
                    if (y == 0 || y == Y_RESOLUTION - 1)
                        D[(y * X_RESOLUTION + x)] = WHITE_COLOR_CODE; // White horizontal borders
                    else if (x == 0 || x == X_RESOLUTION - 1)
                        D[(y * X_RESOLUTION + x)] = WHITE_COLOR_CODE; // White vertical borders
                }
            }
            _logger.Log(LogMessageType.Info, "Video memory initalized");
        }

        public void InitRegisterFile() {
            V = new byte[REGISTER_FILE_SIZE];
            _logger.Log(LogMessageType.Info, "Register file initialized");
        }

        public void InitStack() {
            Stack = new Stack<ushort>();
            _logger.Log(LogMessageType.Info, "Stack initialized");
        }

        public void Start() {
            if (Paused)
                UnPause();
            else {
                InitWorker();
                _worker.RunWorkerAsync();
            }
        }

        private bool Paused { get; set; }

        public void Stop() {
            Paused = false;
            _worker.CancelAsync();
            _logger.Log(LogMessageType.Info, "Program stopped");
            Reset(PC_INIT_VALUE);
        }

        public void Reset(ushort addr) {
            _logger.Log(LogMessageType.Info, "Reset device");
            InitStack();
            InitRegisterFile();
            InitVideoMemory();

            SetPCI(addr);

            ClearTimers();
            _logger.Log(LogMessageType.Info, "Device reset");
        }

        public void Pause() {
            Paused = true;
            _logger.Log(LogMessageType.Info, "Device paused");
        }


        /* Start sync. Used for TESTS only */
        public void StartSync() {
            if (Paused)
                UnPause();
            else {
                InitWorker();
                RunProgram(this, new DoWorkEventArgs(null));
            }
        }

        private void UnPause() {
            Paused = false;
            _logger.Log(LogMessageType.Info, "Device unpaused");
        }

        public bool IsPressed(byte key) {
            return _keys[key];
        }

        public void PressKey(byte key) {
            if (IsPressed(key))
                return;
            //_logger.Log(LogMessageType.Info, "Key {0} pressed", key);
            _keys[key] = true;
        }

        public void ReleaseKey(byte key) {
            if (!IsPressed(key))
                return;
            //_logger.Log(LogMessageType.Info, "Key {0} released", key);
            _keys[key] = false;
        }

        public byte WaitForKey() {
            //_logger.Log(LogMessageType.Info, "Waiting for key pressed");
            while (true) {
                for (byte key = 0; key < KEYS_NUM; key++)
                    if (_keys[key]) {
                        //_logger.Log(LogMessageType.Info, "Key {0} pressed", key);
                        return key;
                    }
                Thread.Sleep(10);
            }
        }

        private void InitMemory() {
            M = new byte[RAM_SIZE]; // Clear memory with zeros

            /* Set font symbols (0-F) */
            SetMemSymbol(Symbol.S0, 0, " 11 ");
            SetMemSymbol(Symbol.S0, 1, "1  1");
            SetMemSymbol(Symbol.S0, 2, "1  1");
            SetMemSymbol(Symbol.S0, 3, "1  1");
            SetMemSymbol(Symbol.S0, 4, " 11 ");

            SetMemSymbol(Symbol.S1, 0, "  1 ");
            SetMemSymbol(Symbol.S1, 1, " 11 ");
            SetMemSymbol(Symbol.S1, 2, "  1 ");
            SetMemSymbol(Symbol.S1, 3, "  1 ");
            SetMemSymbol(Symbol.S1, 4, " 111");

            SetMemSymbol(Symbol.S2, 0, "111 ");
            SetMemSymbol(Symbol.S2, 1, "   1");
            SetMemSymbol(Symbol.S2, 2, " 11 ");
            SetMemSymbol(Symbol.S2, 3, "1   ");
            SetMemSymbol(Symbol.S2, 4, "1111");

            SetMemSymbol(Symbol.S3, 0, "111 ");
            SetMemSymbol(Symbol.S3, 1, "   1");
            SetMemSymbol(Symbol.S3, 2, " 11 ");
            SetMemSymbol(Symbol.S3, 3, "   1");
            SetMemSymbol(Symbol.S3, 4, "111 ");

            SetMemSymbol(Symbol.S4, 0, "1  1");
            SetMemSymbol(Symbol.S4, 1, "1  1");
            SetMemSymbol(Symbol.S4, 2, "1111");
            SetMemSymbol(Symbol.S4, 3, "   1");
            SetMemSymbol(Symbol.S4, 4, "   1");

            SetMemSymbol(Symbol.S5, 0, " 111");
            SetMemSymbol(Symbol.S5, 1, "1   ");
            SetMemSymbol(Symbol.S5, 2, " 11 ");
            SetMemSymbol(Symbol.S5, 3, "   1");
            SetMemSymbol(Symbol.S5, 4, "111 ");

            SetMemSymbol(Symbol.S6, 0, " 11 ");
            SetMemSymbol(Symbol.S6, 1, "1   ");
            SetMemSymbol(Symbol.S6, 2, "111 ");
            SetMemSymbol(Symbol.S6, 3, "1  1");
            SetMemSymbol(Symbol.S6, 4, " 11 ");

            SetMemSymbol(Symbol.S7, 0, "1111");
            SetMemSymbol(Symbol.S7, 1, "  1 ");
            SetMemSymbol(Symbol.S7, 2, " 1  ");
            SetMemSymbol(Symbol.S7, 3, "1   ");
            SetMemSymbol(Symbol.S7, 4, "1   ");

            SetMemSymbol(Symbol.S8, 0, " 11 ");
            SetMemSymbol(Symbol.S8, 1, "1  1");
            SetMemSymbol(Symbol.S8, 2, " 11 ");
            SetMemSymbol(Symbol.S8, 3, "1  1");
            SetMemSymbol(Symbol.S8, 4, " 11 ");

            SetMemSymbol(Symbol.S9, 0, " 11 ");
            SetMemSymbol(Symbol.S9, 1, "1  1");
            SetMemSymbol(Symbol.S9, 2, " 111");
            SetMemSymbol(Symbol.S9, 3, "   1");
            SetMemSymbol(Symbol.S9, 4, " 11 ");

            SetMemSymbol(Symbol.SA, 0, "  1 ");
            SetMemSymbol(Symbol.SA, 1, " 1 1");
            SetMemSymbol(Symbol.SA, 2, "1  1");
            SetMemSymbol(Symbol.SA, 3, "1111");
            SetMemSymbol(Symbol.SA, 4, "1  1");

            SetMemSymbol(Symbol.SB, 0, "111 ");
            SetMemSymbol(Symbol.SB, 1, "1  1");
            SetMemSymbol(Symbol.SB, 2, "111 ");
            SetMemSymbol(Symbol.SB, 3, "1  1");
            SetMemSymbol(Symbol.SB, 4, "111 ");

            SetMemSymbol(Symbol.SC, 0, " 11 ");
            SetMemSymbol(Symbol.SC, 1, "1  1");
            SetMemSymbol(Symbol.SC, 2, "1   ");
            SetMemSymbol(Symbol.SC, 3, "1  1");
            SetMemSymbol(Symbol.SC, 4, " 11 ");

            SetMemSymbol(Symbol.SD, 0, "111 ");
            SetMemSymbol(Symbol.SD, 1, "1  1");
            SetMemSymbol(Symbol.SD, 2, "1  1");
            SetMemSymbol(Symbol.SD, 3, "1  1");
            SetMemSymbol(Symbol.SD, 4, "111 ");

            SetMemSymbol(Symbol.SE, 0, "1111");
            SetMemSymbol(Symbol.SE, 1, "1   ");
            SetMemSymbol(Symbol.SE, 2, "111 ");
            SetMemSymbol(Symbol.SE, 3, "1   ");
            SetMemSymbol(Symbol.SE, 4, "1111");

            SetMemSymbol(Symbol.SF, 0, "1111");
            SetMemSymbol(Symbol.SF, 1, "1   ");
            SetMemSymbol(Symbol.SF, 2, "111 ");
            SetMemSymbol(Symbol.SF, 3, "1   ");
            SetMemSymbol(Symbol.SF, 4, "1   ");

            _logger.Log(LogMessageType.Info, "RAM initialized");
        }

        private void SetMemSymbol(Symbol symbol, int row, string str) {
            M[FONT_ADDRESS + (int)symbol * SYMBOL_HEIGHT + row] = Str2Hex(str);
        }

        private static byte Str2Hex(string str) {
            if (str.Length > SPRITE_SIZE_X)
                throw new Exception("Invalid symbol size");
            byte hex = 0;
            for (int i = 0; i < str.Length; i++)
                hex += (byte)(str[str.Length - 1 - i] == '1' ? (1 << SPRITE_SIZE_X - str.Length + i) : 0);
            return hex;
        }

        public ushort GetSymbolAddress(Symbol symbol) {
            return (ushort) (FONT_ADDRESS + (ushort) symbol * SYMBOL_HEIGHT);
        }

        public bool SetSpriteRow(int x0, int y0, byte row) {
            bool flip = false;
            for (int x = 0; x < SPRITE_SIZE_X; x++) {
                int idx = (y0 % Y_RESOLUTION) * X_RESOLUTION + ((x + x0) % X_RESOLUTION);
                bool prev = D[idx] == WHITE_COLOR_CODE;
                bool next = prev ^ ((row >> ((SPRITE_SIZE_X - 1 - x)) & 0x01) == 0x01);
                D[idx] = next ? WHITE_COLOR_CODE : BLACK_COLOR_CODE;
                flip |= prev & (!next);
            }
            return flip;
        }

        public void TickTimers() {
            if (DelayTimer > 0)
                DelayTimer--;
            if (SoundTimer > 0)
                SoundTimer--;
        }
    }

    /* Font symbols */
    public enum Symbol {
        S0,
        S1,
        S2,
        S3,
        S4,
        S5,
        S6,
        S7,
        S8,
        S9,
        SA,
        SB,
        SC,
        SD,
        SE,
        SF,
    }

    /* Opcode groups */
    internal enum OpcodeType : ushort {
        Type_0x0 = 0x0,
        Type_0x1 = 0x1,
        Type_0x2 = 0x2,
        Type_0x3 = 0x3,
        Type_0x4 = 0x4,
        Type_0x5 = 0x5,
        Type_0x6 = 0x6,
        Type_0x7 = 0x7,
        Type_0x8 = 0x8,
        Type_0x9 = 0x9,
        Type_0xA = 0xA,
        Type_0xB = 0xB,
        Type_0xC = 0xC,
        Type_0xD = 0xD,
        Type_0xE = 0xE,
        Type_0xF = 0xF,
    }
}